package org.hawk.gwt.ppc.reflect;

import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import org.hawk.gwt.ppc.Log;
import org.hawk.gwt.ppc.ProcessingUnitsManager;
import org.hawk.gwt.ppc.javacc.JavaParserTreeConstants;
import org.hawk.gwt.ppc.javacc.ParseException;
import org.hawk.gwt.ppc.reflect.lang.ClassDelegate;
import org.hawk.gwt.ppc.scanner.Processor;
import org.hawk.gwt.ppc.scanner.Token;
import org.hawk.gwt.ppc.scanner.TokenChain;
import org.hawk.gwt.ppc.scanner.utils.JavaClassDescriptorUtils;
import org.hawk.gwt.ppc.utils.Name;
import org.hawk.gwt.ppc.utils.StringUtils;

/**
 * Replaces all reflection calls and classes with theirs analogs from the
 * internal implementation. All {@link Object#getClass()} method calls and
 * [ClassName].class properties accesses will be wrapped with internal
 * {@link ClassDelegate#forClass(Class)} calls. Java class will be used as
 * parameter.
 * 
 * @author alex.bereznevatiy@gmail.com
 */
public class ReflectionCallsProcessor implements Processor {
	/*
	 * used for debug only
	 */
	private static Set<String> modifiedClassNames;

	private static final Set<String> reflectionMethodNames;

	static {
		Set<String> rez = new HashSet<String>();
		rez.add("forName");
		rez.add("newInstance");
		rez.add("isInstance");
		rez.add("isAssignableFrom");
		rez.add("getClassLoader");
		rez.add("getPackage");
		rez.add("getInterfaces");
		rez.add("getModifiers");
		rez.add("getSimpleName");
		rez.add("getCanonicalName");
		rez.add("isAnonymousClass");
		rez.add("getFields");
		rez.add("getMethods");
		rez.add("getConstructors");
		rez.add("getField");
		rez.add("getMethod");
		rez.add("getConstructor");
		rez.add("getDeclaredFields");
		rez.add("getDeclaredMethods");
		rez.add("getDeclaredConstructors");
		rez.add("getDeclaredField");
		rez.add("getDeclaredMethod");
		rez.add("getDeclaredConstructor");

		reflectionMethodNames = Collections.unmodifiableSet(rez);
	}

	private int classExpression = -1;

	public void visitStart(TokenChain<? extends Token> iterator, int kind) {
	}

	private boolean isReflectionMethodCall(TokenChain<? extends Token> iterator) {
		int index = iterator.index();
		boolean rez = checkForReflectionCall(iterator);
		iterator.moveTo(index);
		return rez;
	}

	private boolean checkForReflectionCall(TokenChain<? extends Token> iterator) {
		skipParamTypesForward(iterator);
		if (!iterator.hasNext() || !iterator.current().getToken().equals(".")) {
			return false;
		}
		return reflectionMethodNames.contains(iterator.next().getToken());
	}

	private void skipParamTypesForward(TokenChain<? extends Token> iterator) {
		if (!iterator.hasNext() || !iterator.next().getToken().equals("<"))
			return;
		int deep = 1;
		while (deep > 0) {
			if (iterator.next().getToken().equals(">")) {
				deep--;
			} else if (iterator.current().getToken().equals("<")) {
				deep++;
			}
		}
		iterator.next();
	}

	private void processClass(TokenChain<? extends Token> iterator) {
		Token tok = iterator.current();
		if (iterator.previous().getToken().equals(".")
				&& !processJavaLangPackage(iterator)) {
			return;
		}
		replace(tok, Class.class.getSimpleName(), ClassDelegate.class.getName());
		classExpression = tok.getIndex();
	}

	private void replace(Token tok, String val, String replacement) {
		tok.setToken(StringUtils.replaceAll(tok.getRawToken(), val, replacement));
		logClassIsModified();
	}

	private boolean processJavaLangPackage(TokenChain<? extends Token> iterator) {
		if (!iterator.previous().getToken().equals("lang")
				|| !iterator.previous().getToken().equals(".")
				|| !iterator.previous().getToken().equals("java")
				|| iterator.previous().getToken().equals(".")) {
			return false;
		}

		replace(iterator.next(), "java", "");
		replace(iterator.next(), ".", "");
		replace(iterator.next(), "lang", "");
		replace(iterator.next(), ".", "");
		return true;
	}

	public void visitEnd(TokenChain<? extends Token> iterator, int kind)
			throws ParseException, IOException {
		if (kind == JavaParserTreeConstants.JJTPRIMARYSUFFIX
				&& iterator.previous().getToken().equals("(")) {
			iterator.previous();
			iterator.previous();
			if (iterator.previous().getToken()
					.equals(Class.class.getSimpleName())
					&& isReflectionMethodCall(iterator)) {
				processClass(iterator);
				return;
			}
			iterator.next();
			iterator.next();
		}
		if (kind == JavaParserTreeConstants.JJTPRIMARYSUFFIX) {
			processReflectionExpression(iterator);
		} else if (kind == JavaParserTreeConstants.JJTPRIMARYEXPRESSION
				&& classExpression >= 0) {
			processClassExpression(iterator);
		}
	}

	private void processClassExpression(TokenChain<? extends Token> iterator) {
		while (iterator.previous().getIndex() > classExpression
				&& !isReflectionMethodCall(iterator))
			;
		while (iterator.previous().getIndex() >= classExpression) {
			if (isReflectionMethodCall(iterator)) {
				classExpression = -1;
				return;
			}
		}
		iterator.moveTo(classExpression + 2);
		if (!iterator.next().getToken().equals("(")) {
			return;
		}
		int deep = 1;
		while (deep > 0) {
			if (iterator.next().getToken().equals("(")) {
				deep++;
			} else if (iterator.next().getToken().equals(")")) {
				deep--;
			}
		}
		if (!iterator.next().equals(".")) {
			replace(iterator.current(), ")", ").toClass()");
		}
		classExpression = -1;
	}

	private void processReflectionExpression(
			TokenChain<? extends Token> iterator) {
		while (!iterator.previous().getToken().equals(".")) {
			if (!iterator.hasPrevious()) {
				return;
			}
		}
		iterator.previous();
		if (isReflectionMethodCall(iterator)) {
			processReflectionCallExpression(iterator);
		}
	}

	private void processReflectionCallExpression(
			TokenChain<? extends Token> iterator) {
		try {
			iterator.next();
			int end = iterator.index();
			int start = getReflectionExpressionStart(iterator);
			if (iterator.next().isModified()) {
				return;
			}
			String type = JavaClassDescriptorUtils
					.evaluateReturnTypeOfExpression(iterator, guessClassName(),
							start, end);
			if (Name.getRawName(type).equals(Class.class.getName())) {
				processReflectionExpression(iterator, start, end - 1);
			}
		} catch (Throwable th) {
			Log.internalError(
					"Unable to evaluate reflection expression because "
							+ "of underline error.\nBecause of this error some of the "
							+ "reflection calls may not be correctly recognized by "
							+ "GWT compiller.\n", th);
		}
	}

	private int getReflectionExpressionStart(
			TokenChain<? extends Token> iterator) {
		int deep = 1;
		while (deep > 0) {
			String token = iterator.previous().getToken();
			if (token.equals("return")) {
				deep--;
			}
			if (token.length() != 1)
				continue;
			char t = token.charAt(0);
			if (t == ')' || t == '}' || t == ']') {
				deep++;
			} else if (t == '(' || t == '{' || t == ';') {
				deep--;
			}
			if (deep == 1 && t == '=') {
				deep--;
			}
		}
		return iterator.index() + 1;
	}

	private void processReflectionExpression(
			TokenChain<? extends Token> iterator, int start, int end) {
		iterator.moveTo(start);
		Token startTok = iterator.current();
		iterator.moveTo(end);
		Token endTok = iterator.current();
		StringBuilder prefix = new StringBuilder();
		prefix.append(ClassDelegate.class.getName());
		prefix.append(".forClass(");
		prefix.append(startTok.getToken());
		replace(startTok, startTok.getToken(), prefix.toString());
		replace(endTok, endTok.getRawToken(), endTok.getRawToken() + ')');
	}

	private String guessClassName() {
		String className = ProcessingUnitsManager.getInstance()
				.getCurrentResource();
		if (className == null) {
			// this branch will not work on real runtime
			// but useful for unit testcases.
			className = System.getProperty("hawk.gwt.ppc.current_class");
		}
		return className;
	}

	private boolean isDebugForbided = false;

	private void logClassIsModified() {
		if (isDebugForbided)
			return;
		if (!Log.isDebugEnabled()) {
			isDebugForbided = true;
			return;
		}
		if (modifiedClassNames == null) {
			modifiedClassNames = new HashSet<String>();
		}
		String className = guessClassName();
		if (modifiedClassNames.contains(className)) {
			return;
		}
		modifiedClassNames.add(className);
		Log.debug("\t" + getClass().getSimpleName() + " processing "
				+ className);
	}
}
